Skip to content

iceoryx2 动态类型:Slice 动态 payload 与多段扩容 ​

iceoryx2 的 publish-subscribe 模式默认使用固定大小的 payload 类型,消息长度在编译期确定。当消息大小无法提前知道(传感器帧、变长字节流)时,就需要动态类型——C++ 侧的 bb::Slice<T>、Rust 侧的 [T]。本文覆盖动态类型的 C++/Rust 用法、AllocationStrategy 分配策略,以及多段共享内存扩容、PointerOffset 编码等底层机制,适合已经在用 iceoryx2、需要发送变长消息的开发者。

固定类型 vs 动态类型 ​

方面 固定类型 (FixedSize) 动态类型 (Dynamic)
C++ Payload MyStruct, uint64_t bb::Slice<uint8_t>
Rust Payload MyStruct, u64 [u8]
内部标记 TypeVariant::FixedSize TypeVariant::Dynamic
内存段 静态 SharedMemory<PoolAllocator> ResizableSharedMemory(可多段扩容)
loan API loan_uninit() → 单个元素 loan_slice_uninit(n) → n 个元素
每条消息大小 永远 sizeof(T) 每次 loan 可不同
内存增长 不增长 按 AllocationStrategy 增长
Publisher 额外配置 无 initial_max_slice_len + allocation_strategy
Subscriber 额外配置 无 无(透明接收)

核心思想:Publisher 的共享内存数据段可以在运行时自动扩容,创建新的更大的共享内存段来满足更大的 slice 请求。

C++ 用法 ​

Publisher 端 ​

cpp
#include "iox2/iceoryx2.hpp"

constexpr iox2::bb::Duration CYCLE_TIME = iox2::bb::Duration::from_secs(1);

auto main() -> int {
    using namespace iox2;
    auto node = NodeBuilder().create<ServiceType::Ipc>().value();

    // 步骤1: 声明动态 payload 类型 bb::Slice<T>
    auto service = node.service_builder(ServiceName::create("My/Dynamic/Service").value())
                       .publish_subscribe<bb::Slice<uint8_t>>()
                       .open_or_create()
                       .value();

    // 步骤2: 配置 Publisher 的初始大小和增长策略
    auto publisher = service.publisher_builder()
                         .initial_max_slice_len(16)                        // 初始最多 16 个元素
                         .allocation_strategy(AllocationStrategy::PowerOfTwo) // 不够时按 2 的幂扩容
                         .create()
                         .value();

    uint64_t counter = 0;
    while (node.wait(CYCLE_TIME).has_value()) {
        // 步骤3: 每次请求不同大小的 slice
        uint64_t required_size = (counter + 1) * (counter + 1);
        auto sample = publisher.loan_slice_uninit(required_size).value();

        // 步骤4: 初始化数据
        auto initialized = sample.write_from_fn(
            [&](uint64_t idx) { return static_cast<uint8_t>((idx + counter) % 255); });

        // 步骤5: 发送
        send(std::move(initialized)).value();
        counter++;
    }
}

Subscriber 端 ​

Subscriber 端无需特殊配置,payload 自动解析为 ImmutableSlice:

cpp
#include "iox2/iceoryx2.hpp"

auto main() -> int {
    using namespace iox2;
    auto node = NodeBuilder().create<ServiceType::Ipc>().value();

    auto service = node.service_builder(ServiceName::create("My/Dynamic/Service").value())
                       .publish_subscribe<bb::Slice<uint8_t>>()
                       .open_or_create()
                       .value();

    auto subscriber = service.subscriber_builder().create().value();

    while (node.wait(iox2::bb::Duration::from_secs(1)).has_value()) {
        auto sample = subscriber.receive().value();
        while (sample.has_value()) {
            auto payload = sample->payload();  // bb::ImmutableSlice<const uint8_t>

            // 查询长度
            auto num_elements = payload.number_of_elements();
            auto num_bytes    = payload.number_of_bytes();

            // 按索引访问
            auto first = payload[0];

            // range-for 遍历
            for (const auto& byte : payload) {
                // ...
            }

            sample = subscriber.receive().value();
        }
    }
}

初始化 API 汇总 ​

SampleMutUninit 在动态类型下提供三种初始化方式:

cpp
// 方式1: write_from_fn -- 用 lambda 按索引初始化
auto sample = publisher.loan_slice_uninit(100).value();
auto initialized = sample.write_from_fn([](uint64_t i) {
    return static_cast<uint8_t>(i % 255);
});
send(std::move(initialized)).value();

// 方式2: write_from_slice -- 从已有 slice 拷贝
uint8_t data[] = {1, 2, 3, 4, 5};
bb::ImmutableSlice<uint8_t> source(data, 5);
auto sample2 = publisher.loan_slice_uninit(5).value();
auto initialized2 = sample2.write_from_slice(source);
send(std::move(initialized2)).value();

// 方式3: 手动 placement new + assume_init
auto sample3 = publisher.loan_slice_uninit(10).value();
auto slice = sample3.payload_mut();  // bb::MutableSlice<uint8_t>
for (uint64_t i = 0; i < slice.number_of_elements(); ++i) {
    new (&slice[i]) uint8_t(42);
}
auto initialized3 = assume_init(std::move(sample3));
send(std::move(initialized3)).value();

Publisher 还提供了零拷贝和拷贝两种发送风格:

cpp
// 零拷贝: loan -> init -> send (推荐)
auto sample = publisher.loan_slice_uninit(n).value();
// ... 初始化 ...
send(std::move(initialized)).value();

// 拷贝: send_slice_copy (简单但多一次 memcpy)
uint8_t data[] = {1, 2, 3};
bb::ImmutableSlice<uint8_t> slice(data, 3);
publisher.send_slice_copy(slice).value();

Rust 用法 ​

rust
use iceoryx2::prelude::*;

let node = NodeBuilder::new().create::<ipc::Service>()?;

// payload 类型为 [u8] (Rust 的 unsized slice type)
let service = node
    .service_builder(&"My/Dynamic/Service".try_into()?)
    .publish_subscribe::<[u8]>()
    .open_or_create()?;

let publisher = service
    .publisher_builder()
    .initial_max_slice_len(16)
    .allocation_strategy(AllocationStrategy::PowerOfTwo)
    .create()?;

let required_size = 100;
let sample = publisher.loan_slice_uninit(required_size)?;
let sample = sample.write_from_fn(|idx| (idx % 255) as u8);
sample.send()?;

AllocationStrategy 分配策略 ​

cpp
enum class AllocationStrategy : uint8_t {
    BestFit,     // 精确匹配,节省内存,重分配频繁
    PowerOfTwo,  // 向上取 2 的幂,减少重分配,可能浪费内存
    Static       // 不增长,超出 initial_max_slice_len 则返回 OutOfMemory
};

增长行为示例(initial_max_slice_len = 16):

text
请求长度:    16  →  20  →  50  →  100

BestFit:     16  →  20  →  50  →  100   每次新建精确大小的 segment
PowerOfTwo:  16  →  32  →  64  →  128   每次新建 2^n 大小的 segment
Static:      16  →  ERR →  ERR →  ERR   超过 16 就返回 LoanError::ExceedsMaxLoanSize

选择建议:

场景 推荐策略
消息大小变化剧烈(如传感器帧) PowerOfTwo
消息大小变化很小 BestFit
消息大小固定(已知最大值) Static
对延迟极敏感(避免运行时扩容) Static + 设足够大的 initial_max_slice_len

底层原理 ​

TypeVariant 标记 ​

Rust 核心定义(iceoryx2/src/service/static_config/message_type_details.rs):

rust
#[repr(C)]
pub enum TypeVariant {
    FixedSize,  // 编译期固定大小
    Dynamic,    // 运行时动态大小 (slice)
}

C FFI 映射(iceoryx2-ffi/c/src/api/service_builder_pub_sub.rs):

rust
#[repr(C)]
pub enum iox2_type_variant_e {
    FIXED_SIZE,
    DYNAMIC,
}

C++ 自动推断(iceoryx2-cxx/include/iox2/service_builder_publish_subscribe.hpp):

cpp
auto type_variant = bb::IsSlice<Payload>::VALUE
    ? iox2_type_variant_e_DYNAMIC
    : iox2_type_variant_e_FIXED_SIZE;

bb::IsSlice<T> 是编译期 trait:对 bb::Slice<T> 返回 true,其他类型返回 false。用户无需手动设置,模板会自动推断。

类型信息的传递链路 ​

text
C++ 模板参数 bb::Slice<uint8_t>
  │
  ▼  bb::IsSlice<Payload>::VALUE → true
ServiceBuilder::set_parameters()
  │  type_variant = iox2_type_variant_e_DYNAMIC
  │  type_name = "u8", size = 1, alignment = 1
  ▼
iox2_service_builder_pub_sub_set_payload_type_details()    ← C FFI 函数
  │
  ▼  TypeVariant::Dynamic
Rust ServiceBuilder → MessageTypeDetails { payload: TypeDetail { variant: Dynamic, ... } }
  │
  ▼  写入 Service 的 StaticConfig(持久化到文件/共享内存)
其他进程 open 时读取 StaticConfig 并检查兼容性

MessageTypeDetails 与内存布局 ​

每条消息在共享内存中的布局:

text
┌────────────────┬─────────────────┬───────────────────────────────┐
│     Header     │   UserHeader    │        Payload (动态)          │
│  (FixedSize)   │  (FixedSize)    │  T[0] T[1] T[2] ... T[n-1]   │
│  sizeof(Header)│ sizeof(UH)      │  n * sizeof(T)               │
│  align(Header) │ align(UH)       │  align(T)                    │
└────────────────┴─────────────────┴───────────────────────────────┘

sample 总大小的计算公式(message_type_details.rs):

rust
fn sample_layout(&self, number_of_elements: usize) -> Layout {
    Layout::from_size_align_unchecked(
        align(
            header.size + user_header.size + user_header.alignment - 1
                + payload.size * number_of_elements  // <-- 动态部分
                + payload.alignment - 1,
            header.alignment,
        ),
        header.alignment,
    )
}
  • FixedSize 时 number_of_elements = 1,布局创建时就确定
  • Dynamic 时 number_of_elements 由每次 loan_slice_uninit(n) 的参数决定

ResizableSharedMemory 多段扩容 ​

固定类型使用单个 SharedMemory<PoolAllocator>,所有 chunk 大小一样。动态类型使用 ResizableSharedMemory,可以创建多个 segment:

text
loan_slice_uninit(16):  → 从 Segment 0 分配 (chunk_size = 16*sizeof(T))
loan_slice_uninit(20):  → 16 不够!创建 Segment 1 (chunk_size = 32*sizeof(T), PowerOfTwo)
loan_slice_uninit(50):  → 32 不够!创建 Segment 2 (chunk_size = 64*sizeof(T))
loan_slice_uninit(30):  → Segment 1 的 32 够用,从 Segment 1 分配

Segment 0 (SegmentId=0)        Segment 1 (SegmentId=1)        Segment 2 (SegmentId=2)
┌─────────────────────┐   ┌─────────────────────┐   ┌─────────────────────┐
│ Pool: 16 * sizeof(T)│   │ Pool: 32 * sizeof(T)│   │ Pool: 64 * sizeof(T)│
│ [chunk][chunk][...]  │   │ [chunk][chunk][...]  │   │ [chunk][chunk][...]  │
└─────────────────────┘   └─────────────────────┘   └─────────────────────┘

每个 segment 是独立的 POSIX 共享内存对象。旧 segment 不会被删除(已发出的 sample 可能仍在被 subscriber 使用)。

PointerOffset 编码 ​

跨进程传递的不是指针,而是 PointerOffset(一个 u64):

text
PointerOffset (u64):
┌─────────────────────────────────┬──────────────┐
│  offset (高 56 位)               │ SegmentId    │
│  segment payload 区域内的偏移     │ (低 8 位)    │
└─────────────────────────────────┴──────────────┘
  • SegmentId 标识是哪个 segment(0~255)
  • offset 是 segment 内 payload 区域的字节偏移

还原为本进程指针:地址 = payload_start_address(segment_id) + offset

SegmentId 只有 8 位,意味着最多 256 个 segment。使用 PowerOfTwo 策略能有效控制 segment 数量(每次翻倍,从 16 到 2^31 只需约 16 个 segment)。

Subscriber 延迟映射 ​

Subscriber 在收到一个指向新 segment 的 PointerOffset 时,会延迟打开并映射该 segment:

text
Subscriber 收到 PointerOffset(offset=100, segment_id=2)
  │
  ├─ segment_id=2 已映射? → 是 → 直接计算地址
  │
  └─ 未映射? → 打开共享内存 segment 2 → mmap → 缓存映射 → 计算地址

服务兼容性检查 ​

当进程 open 一个已存在的 service 时,TypeVariant 参与兼容性检查:

rust
fn is_compatible_to(&self, rhs: &Self) -> bool {
    // ...
    && self.payload.variant == rhs.payload.variant  // variant 必须一致
    && self.payload.type_name == rhs.payload.type_name
    && self.payload.size == rhs.payload.size
    && self.payload.alignment <= rhs.payload.alignment
}

不兼容的情况(会返回 IncompatibleTypes 错误):

cpp
// 进程 A: 创建 FixedSize 服务
node.service_builder(name).publish_subscribe<uint64_t>().create();

// 进程 B: 尝试用 Dynamic 类型打开 → 失败!
node.service_builder(name).publish_subscribe<bb::Slice<uint64_t>>().open();  // ERROR

兼容的情况:

cpp
// 进程 A 和 B 都使用同一种类型
// 两边都是 bb::Slice<uint8_t> → OK
// 两边都是 MyStruct → OK(且 type_name, size, alignment 必须匹配)

Payload 类型约束 ​

bb::Slice<T> 中的 T 必须满足:

要求 说明
std::is_trivially_copyable 可安全 memcpy
无堆内存 不能含 std::string, std::vector
无指针成员 指针跨进程无效
无虚函数 vtable 指针跨进程无效
std::is_trivially_destructible 析构不能有副作用
#[repr(C)] / 确定内存布局 跨编译单元布局一致

安全的元素类型:

cpp
bb::Slice<uint8_t>        // 字节流
bb::Slice<float>          // 浮点数组
bb::Slice<int32_t>        // 整数数组
bb::Slice<MyPodStruct>    // 自定义 POD struct (无指针、无虚函数)

不安全的元素类型:

cpp
bb::Slice<std::string>    // 堆内存!
bb::Slice<MyClassWithPtr> // 含指针!

实践建议 ​

  1. initial_max_slice_len 尽量设准。如果 90% 的消息在 1KB 以内,设 1024(对 uint8_t),减少运行时扩容。
  2. 选对 AllocationStrategy:变化剧烈选 PowerOfTwo(用内存换少扩容);变化很小选 BestFit(精确但多扩容);大小已知选 Static(零扩容,最高性能)。
  3. 注意 SegmentId 上限。最多 256 个 segment,BestFit 策略下频繁不同大小的请求可能快速耗尽;PowerOfTwo 的指数增长能有效控制。
  4. Subscriber 无需关心动态细节,只管 receive(),payload 长度信息在 number_of_elements() 中。
  5. 避免 SampleMut 跨线程。SampleMut 析构时操作 Publisher 的内部状态,Publisher 不是线程安全的。
  6. 及时释放 Sample。Subscriber 长时间持有 Sample 会阻止 chunk 回收,可能导致 Publisher OutOfMemory。

关键源码索引 ​

路径均相对 iceoryx2 仓库根目录:

主题 文件路径
TypeVariant / TypeDetail 定义 iceoryx2/src/service/static_config/message_type_details.rs
C FFI iox2_type_variant_e 定义 iceoryx2-ffi/c/src/api/service_builder_pub_sub.rs
C++ 自动推断 TypeVariant iceoryx2-cxx/include/iox2/service_builder_publish_subscribe.hpp → set_parameters()
bb::Slice<T> 定义 iceoryx2-bb/cxx/include/iox2/bb/slice.hpp
bb::IsSlice<T> trait iceoryx2-bb/cxx/include/iox2/bb/slice.hpp (底部)
AllocationStrategy 定义 iceoryx2-cxx/include/iox2/allocation_strategy.hpp
PortFactoryPublisher (C++ builder) iceoryx2-cxx/include/iox2/port_factory_publisher.hpp
Publisher loan/send 实现 (C++) iceoryx2-cxx/include/iox2/publisher.hpp
SampleMutUninit 初始化 API (C++) iceoryx2-cxx/include/iox2/sample_mut_uninit.hpp
SampleMut send/drop (C++) iceoryx2-cxx/include/iox2/sample_mut.hpp
Rust Publisher 核心实现 iceoryx2/src/port/publisher.rs
Rust SampleMut 生命周期 iceoryx2/src/sample_mut.rs
PointerOffset 编码 iceoryx2-cal/src/shm_allocator/pointer_offset.rs
SegmentState 引用计数 iceoryx2/src/port/details/segment_state.rs
DataSegment 数据段管理 iceoryx2/src/port/details/data_segment.rs
ResizableSharedMemory iceoryx2-cal/src/resizable_shared_memory/
C++ 动态数据示例 (Publisher) examples/cxx/publish_subscribe_dynamic_data/src/publisher.cpp
C++ 动态数据示例 (Subscriber) examples/cxx/publish_subscribe_dynamic_data/src/subscriber.cpp
Rust 动态数据示例 (Publisher) examples/rust/publish_subscribe_dynamic_data/publisher.rs
最近更新

基于 VitePress 构建